Skip to content

feat(mobile): polish agent chat (persist model, attachments, copy, reasoning default)#4387

Merged
iscekic merged 3 commits into
mainfrom
mobile-agent-chat-polish
Jul 3, 2026
Merged

feat(mobile): polish agent chat (persist model, attachments, copy, reasoning default)#4387
iscekic merged 3 commits into
mainfrom
mobile-agent-chat-polish

Conversation

@iscekic

@iscekic iscekic commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Four self-contained mobile agent-chat improvements, scoped to the mobile app with no backend contract changes:

  • Persisted last-selected model (Feature 1) — new sessions default to the model the user most recently picked in the same context (per org or personal). Pure helper + SecureStore-backed hook with a hasLoaded guard and variant reset when the persisted variant no longer exists. The hook clears on sign-out alongside other per-user preferences.
  • File attachments (Feature 2) — paperclip button in both the composer and the new-session screen; preview strip with image thumbnails and document chips. Upload path mirrors web: getAttachmentUploadUrl -> PUT to the signed URL. The wire payload ({ path, files }) is forwarded through manager.send and prepareSession, with mutual-exclusion enforcement (no images + attachments together). Slash-command sends are blocked when attachments are queued.
  • Long-press copy on both bubbles (Feature 3) — extracted useMessageCopy + pure collectCopyableText so the same handler runs on user and assistant messages, with iOS ActionSheetIOS and Android immediate copy paths preserved. Adds a success haptic, toast.success('Copied to clipboard'), and accessibilityHint='Long press to copy message text' on both.
  • Configurable reasoning default (Feature 4) — persisted defaultExpanded (collapsed by default), threaded through session-detail-content -> message-bubble -> part-renderer -> reasoning-part-renderer. Surfaced in a settings sheet opened from a small Settings2 button in the chat toolbar; uses an accessible Switch with accessibilityRole='switch'.

Outcome-focused changes only — no shared types or contract changes; no dependency changes.

Verification

  • Manual verification only: set a non-default model in a new session, return -> model is preselected. Switch org -> different model. Attach 1 image + 1 PDF in a new session, confirm upload + reception; 6th file triggers a partial-accept toast; .zip rejected; /command with attachment blocked. Long-press user and assistant messages; iOS shows action sheet, Android copies immediately, haptic fires. Toggle reasoning default, new assistant reasoning renders expanded/collapsed; per-message tap still overrides; setting survives app restart.

Automated checks (run from apps/mobile): pnpm format, pnpm typecheck, pnpm lint, pnpm test, pnpm check:unused all clean. 7 new unit tests added (use-persisted-agent-model, validate, parse-reasoning-default, collect-copyable-text); full suite: 378 tests passing.

Visual Changes

N/A — no visual design changes. Reasoning settings entry appears as a new Settings2 icon in the chat toolbar (right of the mode/model selectors); opening it shows a Modal matching the existing rename-instance-modal pattern. The paperclip button appears in both the composer and the new-session input row. The attachment preview strip mirrors the existing Kilo Chat MessageAttachmentPreviewChip pattern.

Reviewer Notes

  • Pure helpers (agent-model-preference, parse-reasoning-default, collect-copyable-text) are split out from the hooks to keep vitest SSR-transform clean (the hooks pull in expo-secure-store / expo-clipboard / expo-haptics which transitively load react-native's Flow-typed index.js).
  • chat-toolbar.tsx and new.tsx are now over the 300-line cap; both have a /* eslint-disable max-lines */ header with a justification, per apps/mobile/AGENTS.md.
  • mobile-session-manager.ts only forwards attachments when present (conditional spread), so the wire contract stays identical when no attachments are sent.
  • Image pre-resizing is intentionally not implemented (mirrors the documented plan: "if none, upload as-is when already under 5MB and only resize when over"). A future follow-up can add expo-image-manipulator if needed.
  • IDs in the upload hook are generated via Math.random-based strings, not the uuid package, to avoid a new dep.

…asoning default)

- Persist last-selected model per org/personal context and restore it
  when creating a new session
- Add file attachments (images + docs) for new sessions and follow-up
  messages using the existing cloudAgentNext presigned-URL procedures
- Make long-press copy work on both user and assistant messages with
  haptics and accessibility hints
- Add a configurable default for whether reasoning traces render
  expanded, surfaced via a settings entry in the chat toolbar
@iscekic iscekic self-assigned this Jul 3, 2026
iscekic added 2 commits July 3, 2026 18:14
…ference state

- Generate UUIDs (expo-crypto) for attachment path/id: the upload-url and
  attachments schemas validate them with z.uuid(), so the previous
  Date.now/Math.random ids failed every request
- Reference the server-returned R2 key basename in the wire payload instead
  of a client-invented filename that never matched the stored object
- Always derive contentType from the extension map: OS pickers report
  generic types the backend's allowed-type enum rejects
- Require a non-empty prompt to send/create (backend enforces min(1));
  attachments-only sends previously 400ed after destroying the uploads
- Gate the composer paperclip on the manager's supportsAttachments atom;
  non-cloud-agent sessions throw on attachment sends
- Back the reasoning and model preferences with shared module-level
  SecureStore stores (useSyncExternalStore) so the settings toggle applies
  immediately and concurrent hook instances stop clobbering each other's
  writes; clear them on sign-out
- Restore the !model guard in the new-session auto-select effect
- Fix prompt measurement: single onLayout source and padding constants
  matching the actual px-2/py-2 input box
- Move attachment side effects out of the setState updater (double-invoke
  safe), upload via Blob from the file URI, and drop the unbound double cast
- Cleanups: reuse formatFileSize from @kilocode/kilo-chat, single
  AgentAttachmentWire type at all call sites, picker returns candidates
  directly, memoized upload-hook return, max-files constant, settings modal
  only mounted when shown
# Conflicts:
#	apps/mobile/src/components/agents/session-detail-content.tsx
@iscekic iscekic requested a review from jeanduplessis July 3, 2026 16:34
@iscekic iscekic enabled auto-merge (squash) July 3, 2026 16:34
@iscekic iscekic merged commit 2f13d96 into main Jul 3, 2026
18 checks passed
@iscekic iscekic deleted the mobile-agent-chat-polish branch July 3, 2026 16:56
iscekic added a commit that referenced this pull request Jul 3, 2026
Adopts #4387's per-context local model persistence (agent-model-preference
store + usePersistedAgentModel) as the local cache layer, replacing this
branch's simpler single-entry SecureStore hook and its storage key.
useAutoSelectModel now resolves the local fallback via
resolveModelForContext; the server lastSelected -> local -> org default ->
first-available precedence is unchanged. Model changes in an existing
session (session-detail-content) now also write through to the server
preference, mirroring the new-session screen.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants